Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid prepend accumulation problems #3474

Closed

Conversation

elia
Copy link
Member

@elia elia commented Jan 9, 2020

Description
Loading decorators within to_prepare with prepend or include
ends up adding multiple copies of the decorator to the ancestors of
the decorated class. This typically causes trouble when using super
within methods or when changes done in prepended or included are
not idempotent.

Clearing dependencies also solves problems related to overwriting
constants and removed methods (i.e. when a method is removed by a
decorator, previous instances of the decorator that are still in the
ancestors list will still have that method defined and will intercept
calls to it).

Checklist:

  • I have followed Pull Request guidelines
  • I have added a detailed description into each commit message
  • I have updated Guides and README accordingly to this change (if needed)
  • I have added tests to cover this change (if needed)
  • I have attached screenshots to this PR for visual changes (if needed)

initializer 'spree.decorators' do |app|
# Ensure decorated constants are unloaded before reloading decorators
config.to_prepare_blocks.unshift -> { ActiveSupport::Dependencies.clear }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this clear all dependencies declared in all to_prepare blocks of the application? What if the app or other gems added dependencies? Could this be breaking?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tvdeyen that's why I ended up putting it at the beginning with unshift, anyway, although it helps with warnings about redefined constants it doesn't help with the initial problem I was trying to solve.

Digging deeper I've found that when a constant is loaded during the initialization (e.g. inside config/initializers/_spree.rb) it won't be reloaded properly and any decorator will end up being prepended at each reload. Aside from that was also evident that to_prepare is called twice during reloads, only aggravating the problem.

Right now I'm still experimenting but a partial solution seem to involve a combo of an initializer block plus app.reloader.to_run, this solves a lot and relies on the call to ActiveSupport::Dependencies.clear that rails already does within the reload cycle, but still doens't solve the problem for constants loaded during initialization.

I'll make sure to mark the PR as ready when I'm more confident that the solution is proven

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for looking into this and the detailed explanation. 💯

elia added 2 commits March 13, 2020 17:16
Loading decorators within `to_prepare` with `prepend` or `include`
ends up adding multiple copies of the decorator to the ancestors of
the decorated class. This typically causes trouble when using `super`
within methods or when changes done in `prepended` or `included` are
not idempotent.

Clearing dependencies also solves problems related to overwriting
constants and removed methods (i.e. when a method is removed by a
decorator, previous instances of the decorator that are still in the
ancestors list will still have that method defined and will intercept
calls to it).
@elia elia force-pushed the elia/avoid-prepend-accumulation-problems branch from c582817 to c588e40 Compare March 20, 2020 13:39
@kennyadsl
Copy link
Member

I think this problem is similar to the one we faced on extensions on solidus_support. We ended up with this module that handle it.

The trick was to add the decorator folders to the Rails autoload_paths in order to let Rails taking care of unloading everything before core reload, see solidusio/solidus_support#43 and solidusio/solidus_support#44.

TL;DR: We should suggest using a better directory structure for decorators (whose content match module/class names) for example:

# app/decorators/models/my_app/spree/product/add_a_feature.rb
# which defines

module MyApp
  module Spree
    module Product
      module AddAFeature
        # super cool code here

        ::Spree::Product.predend self
      end
    end
  end
end

and change this PR in something like:

application <<-RUBY

decorators_path = root.join("lib/decorators/#{engine}")
config.autoload_paths += decorators_path.glob('*')

initializer 'spree.decorators' do |app|
  # Load application's decorators
  config.to_prepare do
    decorators_path.glob('**/*.rb') do |decorator_path|
      require_dependency(decorator_path)
    end
  end
end

RUBY

@waiting-for-dev waiting-for-dev added the changelog:solidus_core Changes to the solidus_core gem label Aug 30, 2022
@waiting-for-dev
Copy link
Contributor

The code for decorator loading is no longer generated, so we can close it.

@waiting-for-dev waiting-for-dev deleted the elia/avoid-prepend-accumulation-problems branch September 8, 2022 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog:solidus_core Changes to the solidus_core gem
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants